文章
服务器与存储开发
2011 年 2 月更新
作者:Darryl Gove
如何通过使用一组最佳编译器选项和最新编译器进行编译,从运行于最新 Oracle Solaris 平台上的 UltraSPARC 或 x86/EMT64 (x64) 处理器中获得最佳性能?本文就此提供了建议。您应尝试采纳这些建议,但在发布最终程序版本之前,您应准确了解您要求编译器做些什么。
编译程序时,您需要问两个问题:
对于在代码中做出的假设,我有哪些了解?
这两个问题的答案将决定您应使用的编译器选项。
您希望自己的代码在哪些平台上运行?对平台的选择将决定以下各项:
缓存配置
前三项通常对应用程序性能的影响最大。
UltraSPARC (Oracle SPARC) 和 x64 系列处理器可以运行 32 位和 64 位两种代码。与 32 位代码(应用程序和数据的大小限制为 4GB)相比,64 位代码的关键优势在于应用程序可以处理更大的数据集。但是,这种更大地址空间的代价是应用程序占用更大内存空间;长整型变量和指针的大小从 4 个字节增加到 8 个字节。空间占用的增加会使 64 位应用程序比 32 位版本运行速度更慢。
但是,与运行 32 位代码相比,x86/x64 平台在运行 64 位代码时具有一些架构优势。具体来说,应用程序可以使用更多的寄存器,并且可以使用更好的调用约定。在 x86 处理器上,除非应用程序的内存占用大幅增加,否则这些优势通常使 64 位版本的应用程序比 32 位版本的相同代码运行得更快。
UltraSPARC 系列处理器已设计为支持 32 位版本的应用程序使用 64 位指令集的架构特性。因此,从 32 位迁移到 64 位代码不会提高架构性能。所以,对于 UltraSPARC 处理器,仅关注内存占用增加产生的额外成本。
因此,如果 SPARC 二进制文件编译为 32 位,而 x86 二进制文件编译为 64 位,则可能获得最佳性能。确定生成 32 位还是 64 位二进制文件的编译器标志为 -m32 和 -m64 标志。
有关从 32 位迁移到 64 位代码的其他详细信息,请参见将 32 位应用程序转换为 64 位应用程序:考虑事项和通过 Sun Studio 10 工具集实现 64 位 x86 迁移、调试和调优
编译器的默认设置是生成“通用”二进制文件,即,适用于所有平台的二进制文件。在很多情况下,这将是最佳选择。但是,有些情况适于选择不同的目标:
覆盖先前目标设置。编译器从左到右评估选项。如果在编译行中指定了 -fast 标志,则可能适于以另一种选择覆盖 -xtarget=native 的隐式设置。
利用特定处理器的特性。例如,较新的处理器往往具有更多的特性。编译器可以使用这些特性,只不过生成的二进制文件无法在没有这些特性的老旧处理器上运行。
-xtarget 标志实际上设置三个标志:
-xarch 标志指定计算机的架构。从根本上说,这是编译器可以使用的指令集。如果运行应用程序的处理器不支持相应的架构,则应用程序可能无法运行。
-xchip 标志告诉编译器采用哪个处理器运行代码。当编译器针对同一操作具有多种编码方法可供选择时,这会告诉编译器哪些指令模式比较有利。它还告诉编译器用于调度指令的指令延迟,以最大程度减少延时。
-xcache 标志告诉编译器要采用的缓存层次结构。这会极大地影响浮点代码,让编译器能够就如何安排循环以便所处理的数据适合缓存进行选择。
这三个性能设置的影响将取决于应用程序的特点。花费时间进行浮点计算的代码往往表现得对用于目标的设置最为敏感。
默认的 -xtarget=generic 选项应适用于大多数情况。编译器将生成使用 SPARC V8 指令集的 32 位二进制文件,或生成使用 SPARC V9 指令集的 64 位二进制文件。最常见的可能需要不同设置的情况是代码执行大量浮点计算。在这种情况下,使用硬件浮点乘法累加(FMA 或 FMAC)指令会比较有效。
SPARC64 系列处理器支持 FMA 指令。这些指令将浮点乘法和浮点加法(或减法)组合为单一运算。FMA 指令通常采用与浮点加法或浮点乘法相同数量的循环就能完成,因此使用这些指令可以显著提高性能。但是,经过编译使用 FMA 指令的应用程序得出的结果可能不同于经过编译不使用这些指令的同一应用程序得出的结果。
FMAC 指令执行以下运算,称为“融合乘法累加”:
Result = ROUND( (value1 * value2) + value3)
这里的 ROUND 表示在将值存储为结果时,会将其四舍五入到最接近的可表示浮点数。这一条 FMAC 指令取代以下两条指令:
tmp = ROUND(value1 * value2)
Result = ROUND(tmp + value3)
注意包含两条指令的版本具有两次四舍五入运算,这一差别可导致计算出的结果的最低有效位存在差异。
为了生成 FMA 指令,需要使用两个标志编译二进制文件:一个用于指定支持 FMA 指令的架构,另一个用于告诉编译器使用这些指令是可接受的:
-xarch=sparcfmaf -fma=fused
另外,-xtarget=sparc64vi -fma=fused 标志也支持生成 FMA 指令,并且还告诉编译器在编译代码时采用 SPARC64 VI 处理器的特性。这将针对 SPARC64 VI 平台生成最佳代码。经过编译包含 FMA 指令的代码只能在支持这些指令的平台上运行。
默认情况下,编译器以基于 x86 的 32 位通用处理器为目标,因此生成的代码能够在从 Pentium Pro 到 AMD Opteron 架构的任何 x86 处理器上运行。虽然这样生成的代码可以在最广泛的处理器上运行,但没有利用最新式处理器提供的扩展。现有最新式 x86 处理器具有 SSE2 指令集扩展。为了利用这些指令,应使用 -xarch=sse2 标志。但是,编译器可能无法识别出所有使用这些指令的机会,除非还使用向量化标志 -xvector=simd。
因此,对于 x86/x64 处理器,至少使用以下标志进行编译:
-xarch=sse2 -xvector=simd下表汇总了用于各种处理器和架构的选项。
地址空间 | SPARC | SPARC64 |
|---|---|---|
32 位 |
|
|
64 位 |
|
|
地址空间 | x86 | x64/sse2 |
|---|---|---|
32 位 |
|
|
64 位 |
| |
使用优化标志进行编译将改变三个重要特性:编译后应用程序的运行时间、编译所用的时间长度以及最终二进制文件可能具有的调试信息量。通常,优化级别越高,应用程序运行速度越快(编译所用时间越长),提供的调试信息越少。但是,优化级别的具体影响将因应用程序的不同而不同。
要明白这些,最简单的方法是考虑下表所示的三种优化程度。
目的 | 标志 | 注释 |
|---|---|---|
完全调试 | [无优化标志] | 应用程序将具有完备的调试功能,但几乎不进行任何编译器优化,导致性能较低。 |
优化的 |
| 应用程序将具有良好的调试功能,并进行一组适度的优化,通常导致性能显著提高。 |
高度优化 |
| 应用程序将具有良好的调试功能以及一组完备的编译器优化,通常导致性能更高。 |
注:对于 -O 和更低优化级别的 C++,调试 -g 标志将禁止内联某些方法。这会对二进制文件的性能产生重大影响。-g0 标志将提供调试信息而不禁止内联这些方法。因此,如果需要具有与非调试版本相同级别的性能,则结合使用标志 -g0 与 -O 会很有用。在 Oracle Solaris Studio 12 Update 1 中,已将用于 C++ 的 -g 行为更改为这里所述;但先前版本的 C++ 编译器在使用 -g 标志时始终禁用前端内联。
建议:通常,建议优化级别至少为 -O。但在以下两种情况下,可以考虑更低级别:(1) 需要更详细的调试信息,(2) 程序语义需要将变量视为 volatile,在这种情况下应该将优化级别降低至 -xO2。
如果存在 -g 标志,则编译器将为调试器生成信息。对于较低的优化级别,-g 标志将禁用一些不重要的优化以使生成的代码更易于调试。在较高的优化级别,该标志的存在不会改变生成的代码(或其性能),但要知道在高优化级别,调试器并不总是能够将反汇编的代码与确切的源代码行相关联,也并不总是能够确定在寄存器中寄存的(而不是内存中保存的)本地变量的值。
如前所述,在低优化级别,使用 -g 编译器标志时,C++ 编译器将禁用编译器执行的一些内联。但是,-g0 标志将告诉编译器执行它通常执行的所有内联并生成调试信息。
使用 -g 标志进行编译的一个非常重要的原因是,生成的调试信息让 Oracle Solaris Studio Performance Analyzer 能够将程序所花的时间直接归于源代码行,从而使发现性能瓶颈的过程变得相当容易。而且,如果应用程序生成核心文件,调试器通常能够报告生成核心文件的代码行。
建议:始终使用 -g 或 -g0 进行编译。这很少产生任何性能差异,而您的程序将更易于调试和分析。
-fast 提高性能优化代码时,-fast 标志是一个好的起点。但是,它可能无法针对完成的程序提供一组合适的优化。-fast 标志是一个宏,可实现一组完备的优化,通常可让许多应用程序实现接近最佳的性能。但是,其中一些优化可能不适用于您的特定应用程序。
-fast 标志假设执行编译的平台代表将运行结果二进制文件的计算机类型 (-xtarget=native)。编译器将使用编译平台支持的指令集扩展。如果在部署应用程序的平台上并不支持这些指令,则应用程序可能无法运行。可能需要用 -xtarget 标志覆盖隐含的 -xtarget=native 来指定更通用的目标。
在 x86 平台上,-xregs=frameptr 允许编译器使用帧指针作为未分配的被调用方保存寄存器,这可以提高运行时性能。对于 C 编译器,此选项包括在 -fast 中。使用此标志可能意味着一些工具无法正确地生成调用栈信息。
对于 C 编译器,-fast 标志包括 -xalias_level=basic,该标志声明应用程序在不同数据类型之间不包含指针别名。使用此标志进行编译时,不符合语言标准的代码可能无法正确地运行。将在本文后面的高级编译器选项:C/C++ 指针别名部分中讨论指针别名。
-fast 标志还支持某些浮点优化,这些优化将在下一部分使用 -fast 选项时对浮点运算的意义中讨论。
如果要获得最佳应用程序性能,-fast 标志是一个好的起点。建议在针对您应用程序的生产版本确定一组最终编译器标志之前,检查此标志支持的优化。-#、-xdryrun 或 -V 标志可让编译器输出 -fast 包括的选项,您可以使用该列表为您的应用程序选择合适的选项。
-fast 标志的扩展往往随着每个 Solaris Studio 版本而变化。请参阅编译器手册页面,以获得有关如何确定 Solaris Studio C、C++ 和 Fortran 编译器(分别为 cc、CC 和 f95)使用的 -fast 标志扩展的说明。
-fast 选项时对浮点运算的意义要注意的一个问题是 -fast 隐式包含了某些浮点运算简化。这些简化就是 -fns 和 -fsimple=2 选项,它们允许编译器执行一些不符合 IEEE-754 浮点运算标准的优化,并且还允许编译器放宽浮点表达式重新排序相关语言标准。
使用 -fns,将次正规数(即,太小而无法以正规形式表示的非常小的数)保存为零。对次正规数的计算通常在软件中执行,且计算速度非常慢,因此具有大量次正规数计算的代码运行速度也很慢。次正规数以更少的精度有效数字存储,因此包含许多次正规数的代码不仅运行速度较慢,而且还可能执行不准确的计算。因此,次正规数的存在不仅导致性能问题,而且还需要对计算进行分析。
如果 -fsimple=2,编译器可以像数学课本中告诉我们的那样来处理浮点运算。例如,执行加法的顺序并不重要,通过倒数来以乘法取代除法运算被视为是安全的。在纸上进行这些转换时似乎完全可接受,而且它们可以使性能有所提高,但是当代数变成对精度有限的数值执行真正的数值计算时,它们可导致精度损失。
而且,-fsimple=2 允许编译器在进行优化时假设在浮点计算中使用的数据不是 NaN(非数值)。如果您需要使用 NaN 数据进行计算,或者您的应用程序对执行浮点计算的准确顺序敏感,则不建议使用 -fsimple=2 进行编译。
建议:
使用 -fns 和 -fsimple 标志可以显著提高性能。但是,它们也可能导致精度损失。在生产代码中使用这些标志之前,最好先评估使用这些标志获得的性能提高,并评估应用程序的结果中是否存在任何差异。
对于针对 NaN 数据执行计算的应用程序或者已知对浮点计算顺序敏感的应用程序,要避免使用 -fsimple=2。
有关浮点计算的更多信息,请参见 Sun Studio 数值计算指南。
-xipo 选项在链接时对整个程序执行过程间优化。这意味着在链接时再次检查对象文件,以查看是否有任何进一步优化的机会。最常见的机会是将一个文件中的某些代码内联到另一个文件中的代码。术语内联 意味着编译器以例程中的实际代码取代对此例程的调用。
内联的好处有两个原因,最明显的原因是它消除了调用另一个例程的开销。另一个不太明显的原因是内联可能带来可对目标代码进行的其他优化。例如,假设某例程计算图像中特定点的颜色,方法是获取此点的 x 和 y 位置,并计算此点在包含此图像的内存块中的位置:(image_offset = y * row_length + x)。通过将处理图像内所有像素的例程中的代码内联进来,编译器能够生成这样的代码:将当前偏移量加上 1 即可得到下一个点,而不是必须执行一次乘法和一次加法运算以计算每个点的每个地址。因此,内联可以提高性能。
使用 -xipo 的缺点在于它可能显著增加应用程序的编译时间,并且还可能增加可执行文件的大小。
建议:尝试使用 -xipo 进行编译,看看解性能的提高是否抵得上编译时间的增加。
编译程序时,编译器就程序可能如何流动执行(例如采取哪些分支,不采取哪些分支)做出最佳猜测。对于浮点密集的代码,这通常会带来良好的性能。但是,具有许多分支操作的程序可能无法获得最佳性能。
配置文件反馈可帮助编译器优化程序,因为它为编译器提供有关程序实际采取的路径的真实信息。知道流经代码的关键路线之后,编译器可以确保这些路线经过优化。
为了利用配置文件反馈,首先需要使用 -xprofile=collect 来编译执行应用程序的某个版本,然后使用代表性输入数据运行应用程序以收集一个运行时性能配置文件。然后您使用 -xprofile=use 重新编译,其间会使用收集到的性能配置文件数据。这样做的缺点在于编译周期会显著加长(您要执行两次编译并运行一次应用程序),但编译器可以生成更多的最佳执行路径,这意味着运行速度更快。
代表性数据集应该是这样的一个数据集:它能以类似于应用程序在生产中遇到的实际数据的方式来训练代码的执行。可以通过不同负载多次运行程序,构建代表性数据集。当然,如果代表性数据设法以并不代表实际负载的方式来训练代码,则性能可能不是最佳。但是,常见的情况是代码总是通过类似的路径执行,因此无论数据是否为代表性数据,性能都将提高。
有关确定负载是否为代表性负载的更多信息,请阅读我的文章通过覆盖率和分支分析为配置文件反馈选择代表性训练负载。
建议:
尝试使用配置文件反馈和 -xipo 进行编译,因为配置文件信息还将帮助编译器进行更好的内联选择。
如果程序要处理大型数据集,使用大型页保存数据可能有助于提高性能。页 是连续的物理内存区域。处理器使用虚拟内存,这让处理器能够自由地在物理内存中到处移动数据,甚至将数据存储到磁盘中以及从磁盘中加载数据。但是,使用虚拟内存意味着处理器必须在表中查找虚拟地址,以便找到此数据页在实际内存中的实际物理位置。这需要少量时间,但如果经常发生,则查找表所用的时间可能很长。
对于 SPARC,这些页的默认大小为 8 KB,对于 x86 则为 4 KB。不过,处理器可以使用一系列页大小。使用大型页大小的优点在于处理器将执行更少的查找,但缺点在于处理器可能无法找到足够大的连续内存块,以在其中分配大型页(在这种情况下,将改为分配一组大小更小的页)。
控制页大小的编译器选项为 -xpagesize=size。可选大小因平台而异。在 UltraSPARC 处理器上,允许的大小为 4K、8K、64K、512K、2M、4M、32M、256M、2G 或 16G。例如,将页大小从 8K(默认值)更改为 64K 将使查找次数减少至原来的 1/8。在 x86 平台上,默认页大小为 4K,可用的实际大小通常为 4K、2M、4M 和 1G,具体取决于处理器。
可以使用 trapstat 工具通过页大小检测性能问题,前提是有此工具,并且处理器采用 Oracle Solaris 来处理表查找缓冲区 (TLB) 未命中。或者,当处理器提供对 TLB 未命中事件进行计数的硬件性能计数器时,可以使用 cpustat。
报告特定系统上可用页大小的命令为 pagesize -a。
如果应用程序在运行期间发生大量 TLB 未命中事件,则使用 -xpagesize 设置进行重新编译可能会改善性能。
如果两个指针指向内存中的同一位置,则它们“互为别名”。对于编译器,别名意味着由一个指针寻址的内存中的存储可以改变由另一个指针寻址的内存。这意味着编译器必须非常小心,绝对不在包含指针的表达式中对存储和加载进行重新排序,并且在将新数据存储到内存之后,编译器还可能必须重新加载通过指针访问的内存值。
您可以使用两个标志就如何在程序中使用指针做出断言。这些标志告诉编译器可以在源代码中以怎样的方式使用指针。编译器不会检查是否违反了断言,因此,如果您的代码违反断言,则程序可能无法以预期的方式运行。注意 lint 可以帮助您在特定 -xalias_level 对代码进行一些有效性检查。(请参见 Oracle Solaris Studio 12.2:C 用户指南中的第 4 章“lint 源代码检查工具”。)
以下是两个断言:
-xrestrict 断言传递给函数的所有指针都是受限指针。这表示如果函数获得两个传递给它的指针,则在 -xrestrict 下,编译器可以假定这两个指针永不指向重叠内存。
-xalias_level 表示可以就两个不同指针之间的别名级别做出哪些假定。可以将 -xalias_level 视为有关编码样式的语句。您在告诉编译器您如何以使用的编码样式处理指针。例如,您可以告诉编译器 int* 将从不与 float* 指向同一内存位置。
下表汇总了用于 C (cc) 的 -xalias_level 选项。
| 注释 |
|---|---|
| 任何指针都可以互为别名(默认设置)。 |
| 基本类型彼此不互为别名(例如, |
| 结构指针可以按偏移互为别名。结构指针中具有相同偏移量(以字节为单位)的同一类型的结构成员可以互为别名。 |
| 结构指针按公共字段互为别名。如果两个结构指针的前几个字段具有相同的类型,则它们可能可以互为别名。 |
| 指向包含不同变量类型的结构的指针不互为别名。 |
| 指向具有不同名称的结构的指针不互为别名。(因此,即使结构中的所有元素具有相同的类型,如果它们的名称不同,则这些结构不使用别名。)这是语言标准允许的别名级别。 |
| 没有指向结构内部的指针,并将 |
下表汇总了用于 C++ (CC) 的 -xalias_level 选项。
| 注释 |
|---|---|
| 任何指针都可以互为别名(默认设置) |
| 基本类型不互为别名(与用于 C 的 |
| 与用于 C 的 |
注:
正确地指定 -xrestrict 和 -xalias_level 可以显著提高性能。但是,如果您的代码不符合这些标志的要求,则应用程序的运行结果可能无法预测。
对于 C,-xalias_level=std 表示指针以 1999 ISO C 标准建议的方式操作。可以对符合标准的代码指定此选项。
用于 C 的标志 -fast 包含了 -xalias_level=basic。如果在代码中存在不同的基本类型互为别名的情况,则需要在 -fast 的后面使用 -xalias_level=any 标志,告诉编译器任何指针都可能互为别名。
下面我们来做最后一件事:总结所有上述方面,建议一组好的标志。记住,这组标志可能实际上不适用于您的应用程序,但希望它们将为您提供一个好的起点。
注:特殊情况下,在方括号 ([ ]) 中使用标志。
标志 | 注释 |
|---|---|
| 生成调试信息(对于 C++ 可以使用 |
| 积极优化。 |
| 指定目标平台。 |
| 启用过程间优化。 |
| 使用配置文件反馈进行编译。 |
| 无浮点运算优化。如果必须符合 IEEE-754,则使用此标志。 |
[ | 设置指针别名级别(用于 C 和 C++)。仅在您知道该选项对于程序安全的情况下使用。 |
[ | 使用受限指针(用于 C)。仅在您知道该选项对于程序安全的情况下使用。 |
编译器还接受许多其他选项。在此提供的选项可能让大多数程序实现最显著的性能提高,并且相对易于使用。为您的程序选择编译器选项时,记住以下几点:
仅使用可为您带来性能优势的标志,并且 对代码做出可接受的断言。
有关所有这些选项的详细信息,请参见 Oracle Solaris Studio 编译器用户指南和手册页面。
《Solaris Application Programming》(作者:Darryl Gove)介绍了编译器的使用以及在 Oracle Solaris 中提供的许多其他工具。
Darryl Gove 是 Sun Microsystems Inc.(现在为 Oracle)的编译器性能工程小组的高级工程师,负责分析和优化在当前和未来 UltraSPARC 系统上运行的应用程序的性能。Darryl 在英国南安普敦大学获得运筹学硕士和博士学位。加入 Sun 之前,Darryl 在英国从事过各种软件架构和开发工作。他是《Solaris Application Programming》和《The Developer's Edge》两本书的作者。他还在维护一个关注开发人员问题的博客。